package com.dbflow5.processor.definition;

import com.dbflow5.processor.ClassNames;
import com.dbflow5.processor.DBFlowProcessor;
import com.dbflow5.processor.ProcessorManager;
import com.dbflow5.processor.utils.DependencyUtils;
import com.dbflow5.processor.utils.ElementExtensions;
import com.squareup.javapoet.*;

import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

/**
 * Description: Holds onto a common-set of fields and provides a common-set of methods to output class files.
 */
public abstract class BaseDefinition implements TypeDefinition {
    /**
     * Original definition element.
     */
    public Element element;

    /**
     * Optional [TypeElement]. if the [element] passed in is a type.
     */
    public TypeElement typeElement;
    /**
     * The resolved [TypeName] for this definition. It may be a return type if [ExecutableElement],
     *
     */
    public TypeName elementTypeName;
    public ProcessorManager manager;
    public String packageName;

    /**
     * The [ClassName] referring to the type of the definition. This excludes primitives.
     */
    public ClassName elementClassName = null;


    public ClassName outputClassName = null;

    /**
     * Unqualified name of the [element]. Useful for names of methods, fields, or short type names.
     */
    public String elementName = null;

    public BaseDefinition(Element element, TypeElement typeElement, TypeName elementTypeName, ProcessorManager manager, String packageName) {
        this.element = element;
        this.typeElement = typeElement;
        this.elementTypeName = elementTypeName;
        this.manager = manager;
        this.packageName = packageName;

        elementName = element.getSimpleName().toString();
    }

    public BaseDefinition(ExecutableElement element, ProcessorManager processorManager){
        manager = processorManager;
        try {
            packageName = processorManager.elements.getPackageOf(element).getQualifiedName().toString();
            if(packageName == null){
                packageName = "";
            }
        }catch (Exception e){
            packageName = "";
        }
        this.element = element;
        typeElement = null;
        try {
            elementTypeName = TypeName.get(element.asType());
        } catch (IllegalArgumentException i) {
            // unexpected TypeMirror (usually a List). Cannot use for TypeName.
            elementTypeName = null;
        }

        if(elementTypeName != null && elementTypeName.isPrimitive()){
            elementClassName = null;
        }else {
            elementClassName = ElementExtensions.toClassName(element, manager);
        }

        elementName = element.getSimpleName().toString();
    }

    public BaseDefinition(Element element, ProcessorManager processorManager, String packageName) {
        if(packageName == null){
            packageName = processorManager.elements.getPackageOf(element).getQualifiedName().toString();
        }
        if(packageName == null){
            packageName = "";
        }
        manager = processorManager;
        this.element = element;
        this.packageName = packageName;
        if(element instanceof TypeElement){
            typeElement = ((TypeElement)element);
        }else {
            typeElement = ElementExtensions.toTypeElement(element, ProcessorManager.manager);
        }

        try {
            if(element instanceof ExecutableElement){
                elementTypeName = TypeName.get(((ExecutableElement) element).getReturnType());
            }else {
                elementTypeName = TypeName.get(element.asType());
            }
        } catch (IllegalArgumentException i) {
            processorManager.logError("Found illegal type: ${element.asType()} for ${element.simpleName}");
            processorManager.logError("Exception here: $i");
            elementTypeName = null;
        }

        if(elementTypeName != null && !elementTypeName.isPrimitive()){
            elementClassName = ElementExtensions.toClassName(element, processorManager);
        }

        elementName = element.getSimpleName().toString();
    }

    public BaseDefinition(TypeElement element, ProcessorManager processorManager) {
        manager = processorManager;
        this.element = element;
        this.typeElement = element;
        try {
            this.packageName = processorManager.elements.getPackageOf(element).getQualifiedName().toString();
        }catch (Exception e){
            this.packageName = "";
        }
        this.elementTypeName = TypeName.get(element.asType());


        elementClassName = ElementExtensions.toClassName(element, processorManager);

        elementName = element.getSimpleName().toString();
    }

    public void setOutputClassName(String postfix) {
        String outputName;

        ClassName elementClassName = this.elementClassName;
        if (elementClassName == null) {
            if(elementTypeName instanceof ClassName){
                outputName = ((ClassName) elementTypeName).simpleName();
            }else if(elementTypeName instanceof ParameterizedTypeName){
                outputName = ((ParameterizedTypeName) elementTypeName).rawType.simpleName();
                this.elementClassName = ((ParameterizedTypeName) elementTypeName).rawType;
            }else {
                outputName = elementTypeName.toString();
            }
        } else {
            outputName = elementClassName.simpleName();
        }
        outputClassName = ClassName.get(packageName, outputName + postfix);
    }

    protected void setOutputClassNameFull(String fullName) {
        outputClassName = ClassName.get(packageName, fullName);
    }

    public TypeSpec typeSpec() {
        if (outputClassName == null) {
            manager.logError("$elementTypeName's is missing an outputClassName. Database was " +
                    "${(this as? EntityDefinition)?.associationalBehavior?.databaseTypeName}");
        }

        return publicFinalClass((outputClassName != null && outputClassName.simpleName() != null) ? outputClassName.simpleName() : "", builder -> {
            if (DependencyUtils.hasJavaX()) {
                builder.addAnnotation(at(ClassNames.GENERATED, stringStringMap -> {
                    stringStringMap.put("value", "\"" + DBFlowProcessor.class.getCanonicalName() + "\"");
                    return null;
                }).build());
            }
            if(extendsClass() != null){
                builder.superclass(extendsClass());
            }
            TypeName[] typeNames = implementsClasses();
            for(TypeName typeName : typeNames){
                builder.addSuperinterface(typeName);
            }
            builder.addJavadoc("This is generated code. Please do not modify");
            onWriteDefinition(builder);
            return builder;
        });
    }

    protected TypeName extendsClass() {
        return null;
    }

    protected TypeName[] implementsClasses() {
        return new TypeName[]{};
    }

    public void onWriteDefinition(TypeSpec.Builder typeBuilder) {

    }

    private TypeSpec publicFinalClass(String className, Function<TypeSpec.Builder, TypeSpec.Builder> typeSpecFunc) {
        return typeSpecFunc.apply(TypeSpec.classBuilder(className)).addModifiers(Modifier.PUBLIC, Modifier.FINAL).build();
    }

    private AnnotationSpec.Builder at(ClassName className, Function<Map<String, String>, Void> mapFunc) {
        AnnotationSpec.Builder builder = AnnotationSpec.builder(className);

        Map<String, String> map = new HashMap<>();
        mapFunc.apply(map);
        map.forEach(builder::addMember);
        return builder;
    }
}
